User.php

<?php

namespace Tlf;

class User {

    use \Tlf\User\Permissions;

    ////////
    // Non DB fields
    ////////
    /** user model from sentinel */
    public $pdo;


    /** The key in $_COOKIE['key'] that the login code is stored in */
    static public $cookie_name = 'taeluf_login';

    /** Number of seconds before a new cookie expires */
    public $cookie_expiry = 15552000; //180 days: 60*60*24*180; 
    public $registration_expiry = 432000; //5 days: 60*60*24*5; 
    // public $password_expiry = 946080000; //30 years: 60*60*24*365*30;
    public $password_reset_expiry = 86400; // 1 day: 60*60*24; 
    public $is_logged_in = false;


    ////////
    // database fields
    ////////
    /** email address of user 
     * @todo remove hacky approach to syncing this class's email & the model's email
     */
    public string $email;
    public int $id = -1;
    public bool $is_active = false;


    /**
     * Array of query strings identifiable by key. Generated by LilSql (of LilDb package)
     */
    public array $queries = [];

    public function __construct($pdo){
        $this->pdo = $pdo;
        $this->queries = unserialize(file_get_contents(__DIR__.'/../db/serialized.txt'));
    }

    /** Set values from a database row
     */
    public function from_row(array $row){
        $this->email = $row['email'];
        $this->id = $row['id'];
        $this->is_active = $row['is_active'];
    }
    ////////
    //// user status (registered, active, logged in)
    ////////

    /** 
     * returns true/false whether logged in or not. 
     */
    public function is_logged_in():bool {
        return $this->is_logged_in;
    }


    /**
     * Set the login cookie code via `setcookie()` & also set it to `$_COOKIE` 
     * @return return value from `setcookie()` (true if it sent. False if it's too late to send headers)
     */
    public function set_login_cookie($code){
        $_COOKIE[static::$cookie_name] = $code;
        return setcookie(static::$cookie_name, $code, time()+$this->cookie_expiry, '/', '', true,true);
    }

    /**
     * Change `$user->is_logged_in` to false. `setcookie()` to delete the cookie. de-activate the cookie key in the database
     *
     * @return the old cookie code which SHOULD be disabled now
     */
    public function logout(){
        $code = $_COOKIE[static::$cookie_name] ?? null; 
        setcookie(static::$cookie_name, '', 1);
        $stmt=$this->pdo->prepare($this->queries['user.logout']);
        $stmt->execute(['code'=>$_COOKIE[static::$cookie_name], 'user_id'=>$this->id]);
        unset($_COOKIE[static::$cookie_name]);
        return $code;
    }

    /**
     * Get a login cookie after validating the password. You must call $this->set_login_cookie($code) to actually send the cookie to the browser. Or the `setcookie()` function if you wish to customize it
     *
     * @return string login cookie on success, false on failure
     */
    public function password_login(string $password) {
        if (!$this->is_active
            ||$password===null
            ||$password===''
            ||$password==='0'
        ){
            return false;
        }

        $stmt = $this->pdo->prepare($this->queries['user.get_password']);
        $stmt->execute(['email'=>$this->email]);
        if ($stmt==false){
            return false;
        }
        $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
        if (count($rows)!==1)return false;

        if (!password_verify($password, $rows[0]['password']))return false;


        $code = $this->new_code('login_cookie');

        $this->is_logged_in = true;

        return $code;
    }

    /** Activate a user
     * @param $code a code generated by new_activation_code()
     * @return true if the activation succeeds or if the code is already active. False otherwise
     */
    public function activate(string $code):bool{
        $stmt = $this->pdo->prepare($this->queries['user.activate']);
            $stmt->execute([
                'code'=>$code,
                'user_id'=>$this->id
            ]);

        if ($stmt->rowCount()==0)return false;
        $stmt->nextRowset();
        $type = $stmt->fetch()[0];
        if ($type=='registration'){
            $this->is_active = true;
        }
        return true;
    }

    /** 
     * Generate a new cryptographically secure code (& insert into database)
     *
     * @param $type 'registration' or 'password_reset' or 'login_cookie'
     * @return the code
     */
    public function new_code(string $type): string{
        // i need different expiry for activation codes, reset password codes, and login_cookie codes

        $code = bin2hex(random_bytes(100)); 

        if ($type=='registration')$expiry = $this->registration_expiry;
        else if ($type=='login_cookie')$expiry = $this->cookie_expiry;
        else if ($type=='password_reset')$expiry = $this->password_reset_expiry;

        $data = [
            'code'=>$code,
            'type'=>$type,
            'user_id'=>$this->id,
            'is_active' => 0,
            'expiry'=>$expiry,
        ];

        $activated_at = 'NULL';
        if ($type=='login_cookie'){
            $data['is_active'] = 1;
            $activated_at = 'NOW()';
        }
        $stmt = $this->pdo->prepare(
            sprintf($this->queries['user.new_code'], $activated_at)
        );
        $stmt->execute($data);

        return $code;
    }

    /** 
     * @return int id on success, boolean false on failure
     */
    public function register(string $password){
        $stmt = $this->pdo->prepare($this->queries['user.register']);
        $stmt->execute(
            [
            'email'=>$this->email,
            'password'=>password_hash($password,PASSWORD_DEFAULT),
            ]
        );
        
        $this->id = $this->pdo->lastInsertId();
        // var_dump($this->id);
        // exit;
        return $this->id;
    }

    /**
     * Set a new password on the user. Executes a pdo/mysql UPDATE query.
     *
     * @param $password a string password, as the user typed
     * @param $code a code received from `$this->new_code("password_reset"): string`
     *
     * @return true if password changed. False otherwise
     */
    public function new_password(string $password, string $code): bool{
        $hash = password_hash($password,PASSWORD_DEFAULT);
        //run a query that updates the password hash only if the code is valid
        
        $stmt = $this->pdo->prepare($this->queries['user.new_password']);
        $stmt->execute([
            'password_hash'=>$hash,
            'code'=>$code,
            'user_id'=>$this->id
        ]);

        // echo $code;

        // print_r($this->pdo->errorInfo());
//
        // var_dump($stmt->rowCount());

        if ($stmt->rowCount()!==2)return false;

        return true;
    }
    
    /**
     * Checks if a user has registered for the site
     * @returns true if user exists in database, regardless of activation status
     */
    public function is_registered():bool{
        if ($this->id<0)return false;
        //if there IS a model, this means the user is in the database
        return true;
    }

    /**
     * Check if a user is active (clicked the link in the confirmation email)
     * @return true if user has clicked the link in their activation email. False otherwise
     */
    public function is_active():bool{
        //maybe should load activation status in initial query for the user.
        if ($this->is_active)return true;
        return false;
    }

    public function security_logs($limit = 100){
        $stmt = $this->pdo->prepare(sprintf($this->queries['user.get_logs'], $limit));
        $stmt->execute(
            ['email'=>$this->email]
        );
        $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
        return $rows;
    }

}